import abc
import difflib
import inspect
import random
import sys

import util


HUNT = 'h'
SLACK = 's'


class AbstractPlayer(object):
    __metaclass__ = abc.ABCMeta

    @abc.abstractmethod
    def hunt_choices(self, cur_round, my_food, my_rep, m, reputations):
        pass

    def hunt_outcomes(self, food_earnings):
        pass

    def round_end(self, award, m, number_hunters):
        pass

    def __repr__(self):
        return util.rchop(self.__class__.__name__, 'Player')

    @staticmethod
    def all_subclasses():
        module = inspect.getmembers(sys.modules[__name__], inspect.isclass)
        return [cls for _, cls in module
                if issubclass(cls, AbstractPlayer)
                and cls not in (AbstractPlayer, CourteousPlayer)]

    @staticmethod
    def create_from_string(player_name, **kwargs):
        module = sys.modules[__name__]
        class_members = inspect.getmembers(module, inspect.isclass)

        for class_name, constructor in class_members:
            if player_name.lower() == class_name.lower():
                return constructor(**kwargs)

        class_names = [name for (name, _) in class_members]
        matches = difflib.get_close_matches(player_name, class_names)
        suggestions = ' (did you mean %s?)' % ' or '.join(matches)
        raise TypeError('Unknown Player type "%s"%s'
                        % (player_name, suggestions if matches != [] else ''))


class HuntPlayer(AbstractPlayer):
    """Always cooperates except if the opponent has zero reputation"""

    def hunt_choices(self, cur_round, my_food, my_rep, m, reputations):
        return hunt_with_threshold(reputations, 0)


class SlackPlayer(AbstractPlayer):
    """Always defects"""

    def hunt_choices(self, cur_round, my_food, my_rep, m, reputations):
        return hunt_never(reputations)


class RandomPlayer(AbstractPlayer):
    """Cooperates 50% of the time"""

    def hunt_choices(self, cur_round, my_food, my_rep, m, reputations):
        return hunt_randomly(reputations, 0.5)


class AboveAvgPlayer(AbstractPlayer):
    """Cooperates if partner's reputation is higher than average reputation"""

    def hunt_choices(self, cur_round, my_food, my_rep, m, reputations):
        return hunt_with_threshold(reputations, util.mean(reputations))


class PickyPlayer(AbstractPlayer):
    """Cooperates if partner's reputation is higher than some value"""

    def __init__(self, cooperate=0.5):
        self.cooperate = cooperate

    def hunt_choices(self, cur_round, my_food, my_rep, m, reputations):
        return hunt_with_threshold(reputations, self.cooperate)

    def __repr__(self):
        return (super(PickyPlayer, self).__repr__() +
                '-ht%.2f' % self.cooperate)


class PercentilePlayer(AbstractPlayer):
    """Cooperates if the partner has a top-X percentile reputation"""

    def __init__(self, percentile=90):
        self.percentile = percentile

    def hunt_choices(self, cur_round, my_food, my_rep, m, reputations):
        top_reputation = util.percentile(reputations, self.percentile / 100.0)
        return hunt_with_threshold(reputations, top_reputation)

    def __repr__(self):
        return (super(PercentilePlayer, self).__repr__() +
                '-pt%d' % self.percentile)


class AdaptiveRandomPlayer(AbstractPlayer):
    """Adopts probability of a biased coin proportionally to the
    amount of players dead. More dead players more likely to slack"""

    def __init__(self):
        self.initial_no_players = None

    def hunt_choices(self, cur_round, my_food, my_rep, m, reputations):
        if self.initial_no_players is None:
            self.initial_no_players = float(len(reputations))

        play_no_diff = self.initial_no_players - len(reputations)
        slack_prob = 0.5 + 0.5 * play_no_diff / self.initial_no_players
        return hunt_randomly(reputations, slack_prob)


class TitForTatPlayer(AbstractPlayer):
    """ Uses opponents reputation as a probability for Hunting"""

    def hunt_choices(self, cur_round, my_food, my_rep, m, reputations):
        return [HUNT if random.random() < r else SLACK for r in reputations]


class SocialWellfarePlayer(AbstractPlayer):
    """Wants to get enough people to hunt in order for the tribe to get the
    altruistic reward. If there are N expected hunt events in this round, the
    player will cooperate with the m - N players with the highest reputation in
    order to ensure that the altruistic reward will be triggered. If m - N > P
    (i.e. the player can't alone make the difference between triggering the
    reward and not triggering the reward), the player will cooperate with
    everyone"""

    def hunt_choices(self, cur_round, my_food, my_rep, m, reputations):
        expected_hunts = expected_hunt_events([my_rep] + reputations)
        required_hunts = m - int(expected_hunts)

        if required_hunts >= len(reputations):
            return hunt_with_threshold(reputations, 0)

        if required_hunts <= 0:
            return hunt_never(reputations)

        return hunt_with_top_players(reputations, required_hunts)


class CourteousPlayer(AbstractPlayer):
    """Player who hunts with everyone at the start of the game until there
    have been enough rounds played such that the reputation values are
    meaningful."""

    __metaclass__ = abc.ABCMeta

    def __init__(self, min_decisions=1000, **kwargs):
        self.min_decisions = min_decisions
        self.num_warmup_rounds = None

    @abc.abstractmethod
    def hunt_choices(self, cur_round, my_food, my_rep, m, reputations):
        if self.num_warmup_rounds is None:
            num_players = len(reputations)
            decisions_per_round = num_players * (num_players - 1)
            num_rounds = self.min_decisions / float(decisions_per_round)
            self.num_warmup_rounds = int(max(num_rounds, 1))

        if cur_round <= self.num_warmup_rounds:
            return hunt_with_threshold(reputations, 0)

        return None


class SelfishSocialWellfarePlayer(SocialWellfarePlayer, PickyPlayer):
    """The same as SocialWellfarePlayer with a difference that it changes to
    Picky-player when its food shrinks to |food_threshold| percent of the
    maximum amount of food the player ever had"""

    def __init__(self, food_threshold=0.85, cooperate=1, **kwargs):
        PickyPlayer.__init__(self, cooperate)
        self.local_max_food = 0
        self.food_threshold = food_threshold
        self.local_min_food = 0

    def hunt_choices(self, cur_round, my_food, my_rep, m, reputations):
        if self._be_picky(my_food):
            return PickyPlayer.\
                hunt_choices(self, cur_round, my_food, my_rep, m, reputations)

        return SocialWellfarePlayer.\
            hunt_choices(self, cur_round, my_food, my_rep, m, reputations)

    def _be_picky(self, my_food):
        if my_food > self.local_max_food:
            self.local_max_food = my_food
            self.local_min_food = my_food
        elif my_food > self.local_min_food:
            a = my_food - self.local_max_food
            b = (1 - self.food_threshold) * self.local_min_food
            if a > b:
                self.local_max_food = my_food
        else:
            self.local_min_food = my_food
        food_diff = self.local_max_food - my_food
        return food_diff > (1 - self.food_threshold) * self.local_max_food

    def __repr__(self):
        return (super(SelfishSocialWellfarePlayer, self).__repr__() +
                '-ft%.2f' % (self.food_threshold))


class Player(SelfishSocialWellfarePlayer, CourteousPlayer):
    """Submitted strategy: CourteousSelfishSocialWellfarePlayer"""

    def __init__(self, **kwargs):
        SelfishSocialWellfarePlayer.__init__(self, **kwargs)
        CourteousPlayer.__init__(self, **kwargs)

    def hunt_choices(self, cur_round, my_food, my_rep, m, reputations):
        courteous_hunt = CourteousPlayer.\
            hunt_choices(self, cur_round, my_food, my_rep, m, reputations)
        selfish_hunt = SelfishSocialWellfarePlayer.\
            hunt_choices(self, cur_round, my_food, my_rep, m, reputations)
        return courteous_hunt or selfish_hunt


def hunt_never(reputations):
    return [SLACK for _ in reputations]


def hunt_with_threshold(reputations, threshold):
    return [HUNT if r > threshold else SLACK for r in reputations]


def hunt_with_top_players(reputations, num_players):
    decisions = [None] * len(reputations)

    top_reputations = sorted(reputations, reverse=True)[:num_players]
    for top_reputation in top_reputations:
        for (idx, r) in enumerate(reputations):
            if r == top_reputation:
                decisions[idx] = HUNT
                num_players -= 1
                if num_players == 0:
                    return [SLACK if d is None else d for d in decisions]

    return [SLACK if d is None else d for d in decisions]


def hunt_randomly(reputations, threshold):
        return [HUNT if random.random() > threshold else SLACK
                for _ in reputations]


def expected_hunt_events(hunt_affinities):
    """Returns the expected number of hunt events when player i hunts with
    every other player with probability hunt_affinities[i]"""

    hunts = 0
    for i, reputation in enumerate(hunt_affinities):
        own_choices = [reputation] * (len(hunt_affinities) - i - 1)
        other_choices = hunt_affinities[i + 1:]
        hunts += sum([expected_hunt_events_single_hunt(p, r)
                      for (p, r) in zip(own_choices, other_choices)])

    return hunts


def expected_hunt_events_single_hunt(p, r):
    """Returns the expected number of hunt events (E) in a single hunt where
    the two players will hunt with probabilities p and r respectively.

    We have
        p(hunt, hunt) = p * r
        p(hunt, slack) = p * (1 - r)
        p(slack, hunt) = (1 - p) * r
        p(slack, slack) = (1 - p) * (1 - r)

    Thus E = 2 * p(hunt, hunt) +
             1 * p(hunt, slack) +
             1 * p(slack, hunt) +
             0 * p(slack, slack)
           = 2pr + p-pr + r-pr = p + r
    """

    return p + r
